kanten = zylinder.edges()
for k in kanten:
print(k.geom_type) # CIRCLE oder LINE oder ...
print(k.length)
wire = zylinder.faces()[0].wires()[0]
print(len(wire.edges()), "Kanten im Wire")
→ Flächen können mehrere Wires haben: äußere Kontur + innere Löcher
flaechen = zylinder.faces()
for f in flaechen:
print(f.geom_type) # PLANE oder CYLINDER oder ...
print(f.area)
shell = zylinder.shells()[0]
print(len(shell.faces()), "Flächen in der Shell")
→ Eine geschlossene Shell begrenzt ein Volumen
solid = zylinder.solids()[0]
print(solid.volume) # Volumen in mm³
print(solid.area) # Oberfläche in mm²
Part, Sketch, Curve in build123d sind spezialisierte Compoundsquader = bd.Box(30, 20, 10)
gruppe = bd.Compound([zylinder, quader])
print(len(gruppe.solids()), "Solids in der Gruppe")
| Typ | Dimension | Begrenzt durch | Geometrie |
|---|---|---|---|
| Vertex | 0D | – | Punkt |
| Edge | 1D | Vertices | Kurve |
| Wire | 1D | Edges | – |
| Face | 2D | Wires | Fläche |
| Shell | 2D | Faces | – |
| Solid | 3D | Shells | – |
| Compound | – | beliebig | – |
Die Elemente bilden eine hierarchische Struktur (gerichteter Graph):
Solid
└── Shell
├── Face
│ └── Wire
│ └── Edge
│ └── Vertex
└── Face
└── Wire
└── Edge
└── Vertex ← derselbe Vertex wie oben!
Entscheidend: Teilelemente werden geteilt, nicht kopiert.
Zwei Objekte sind verbunden, wenn sie ein gemeinsames Begrenzungselement teilen:
quader = bd.Box(20, 20, 10)
# Welche Flächen teilen eine bestimmte Kante?
kante = quader.edges()[0]
for f in quader.faces():
if kante in f.edges():
print("Fläche enthält diese Kante:", f.center())
quader = bd.Box(20, 20, 10)
# Alle Kanten einer bestimmten Fläche
oberseite = quader.faces().sort_by(bd.Axis.Z).last
print("Kanten der Oberseite:", len(oberseite.edges()))
# Alle Vertices einer bestimmten Kante
kante = oberseite.edges()[0]
print("Vertices der Kante:", len(kante.vertices()))
→ Die Topologie erlaubt Navigation von oben nach unten durch die Hierarchie
# Quader nach boolescher Operation
quader = bd.Box(40, 40, 20)
loch = bd.Cylinder(radius=8, height=20)
ergebnis = quader - loch
print("Faces: ", len(ergebnis.faces())) # 7
print("Edges: ", len(ergebnis.edges())) # 15
print("Vertices:", len(ergebnis.vertices())) # 10
Boolesche Operationen verändern die Topologie – neue Elemente entstehen, alte entfallen.
Gedankenexperiment: Sechs quadratische Flächen, topologisch zu einer Shell verbunden.
Was entsteht?
Die Topologie ist identisch – 6 Faces, 12 Edges, 8 Vertices, alles verbunden.
→ Wir brauchen eine zusätzliche Information: in welche Richtung zeigt jede Fläche?
Die Lösung: Jede Face trägt eine Richtungsinformation – den Normalenvektor.
Regel: In einem gültigen Solid zeigt die Normale immer vom Material weg (nach außen).
Dieselbe Logik eine Ebene tiefer: Welche Seite einer Fläche ist „innen"?
Regel: Wenn man von außen auf eine Face schaut (entgegen der Normalen), liegt das Material links von der Durchlaufrichtung jeder Kante.
# build123d prüft automatisch die Orientierung
ergebnis = quader - loch
print(ergebnis.is_valid) # True wenn korrekt
→ build123d und OCCT kümmern sich meist automatisch darum – aber das Konzept erklärt, warum manche Operationen fehlschlagen.
Die Topologie-Konzepte dieser Vorlesung sind nicht an eine Software gebunden:
| Software | Kernel | Standard |
|---|---|---|
| CATIA | CGM (Dassault) | B-Rep |
| SolidWorks | Parasolid (Siemens) | B-Rep |
| NX (Unigraphics) | Parasolid (Siemens) | B-Rep |
| FreeCAD | OCCT | B-Rep |
| build123d | OCCT | B-Rep |
→ Vertex, Edge, Wire, Face, Shell, Solid – überall dieselben Konzepte, nur unterschiedliche Syntax.
ISO 10303 (STEP) ist das universelle Austauschformat für B-Rep-Geometrie.
import build123d as bd
# Eine STEP-Datei aus CATIA laden:
fremdes_teil = bd.import_step("catia_teil.step")
# Und sofort mit denselben Methoden arbeiten:
print(len(fremdes_teil.faces())) # Flächen zählen
print(len(fremdes_teil.edges())) # Kanten zählen
fremdes_teil.show_topology() # Struktur anzeigen
Das Gleiche (normiert durch B-Rep / ISO 10303):
Das Unterschiedliche (kernel-spezifisch):
→ Wer B-Rep versteht, kann sich in jedem professionellen CAD-System orientieren.
Jedes Objekt stellt Selektoren bereit, die ShapeList zurückgeben:
part = bd.Box(40, 30, 20) - bd.Cylinder(radius=8, height=20)
part.vertices() # alle Vertices
part.edges() # alle Kanten
part.wires() # alle Wires
part.faces() # alle Flächen
part.solids() # alle Körper
ShapeList ist eine Liste mit Zusatzmethoden zum Filtern und Sortieren.
part = bd.Box(40, 30, 20)
# Nach Position entlang einer Achse
oberseite = part.faces().sort_by(bd.Axis.Z).last # höchste Fläche
unterseite = part.faces().sort_by(bd.Axis.Z).first # tiefste Fläche
# Nach Geometrieeigenschaft
groesste = part.faces().sort_by(bd.SortBy.AREA).last
laengste = part.edges().sort_by(bd.SortBy.LENGTH).last
# Gruppieren
gruppen = part.faces().group_by(bd.SortBy.AREA) # Liste von Listen
from build123d import GeomType
part = bd.Box(30, 20, 10) - bd.Cylinder(radius=5, height=10)
# Nach Achsausrichtung (Kanten parallel, Flächen normal)
part.edges().filter_by(bd.Axis.Z) # vertikale Kanten
part.faces().filter_by(bd.Axis.Z) # Ober-/Unterseite
# Nach Geometrietyp
part.edges().filter_by(GeomType.CIRCLE) # Kreiskanten
part.faces().filter_by(GeomType.PLANE) # ebene Flächen
part.faces().filter_by(GeomType.CYLINDER) # zylindrische Flächen
# Nach Position (Mittelpunkt der Kante/Fläche)
part.edges().filter_by_position(bd.Axis.Z, 4, 6)
Problem: Nach jeder Operation kann sich die interne Nummerierung der Objekte ändern!
part_v1 = bd.Box(40, 30, 20)
# part_v1.faces()[0] ← Index 0 ist eine bestimmte Fläche
part_v2 = bd.fillet(part_v1.edges(), radius=2)
# part_v2.faces()[0] ← Index 0 kann jetzt eine ANDERE Fläche sein!
→ Deshalb: immer semantisch selektieren (sort_by, filter_by), nie hart auf Index verlassen.
| Topologie | Geometrie | |
|---|---|---|
| Beschreibt | Struktur, Verbindungen | Form, Position |
| Fragt | Was ist womit verbunden? | Wo liegt was? |
| Beispiel | 6 Flächen, 12 Kanten | Fläche liegt bei z=10 |
| Ändert sich bei | Bool. Operationen | Skalierung, Verschiebung |
Solid → Shell → Face → Wire → Edge → Vertex
3D 2D 2D 1D 1D 0D
part.faces().sort_by(bd.Axis.Z).last # höchste Fläche
part.edges().filter_by(bd.GeomType.CIRCLE) # Kreiskanten
part.faces().group_by(bd.SortBy.AREA)[-1] # größte Flächen
part.edges().filter_by_position(bd.Axis.Z, 4, 6) # Kanten in Bereich
→ Robuste Selektion durch semantische Kriterien statt Indizes
In der nächsten Einheit: Wie wird Form mathematisch beschrieben?
Übung 2: B-Rep Topologie
David Straub
Nach dieser Übung können Sie:
import build123d as bd
from build123d import GeomType
# Unser Ausgangskörper: Montageplatte mit vier Bohrungen
platte = bd.Box(80, 60, 8)
for x, y in [(-27, -20), (27, -20), (-27, 20), (27, 20)]:
platte = platte - bd.Pos(x, y) * bd.Cylinder(radius=4, height=8)
platte
Untersuchen Sie die Topologie der Montageplatte:
show_topology() auf – was sehen Sie?Hinweise: .show_topology(), .faces(), .edges(), .vertices(), len()
Geben Sie für jede Fläche den Geometrietyp und die Fläche aus. Dasselbe für jede Kante mit Länge.
Hinweise: .geom_type, .area, .length, for f in platte.faces():
Hinweise: .faces(), .sort_by(Axis.Z), .last, .filter_by(GeomType.CYLINDER), .geom_type, .area
radius=1.Hinweise: .edges(), .filter_by(GeomType.CIRCLE), .filter_by_position(Axis.Z, min, max), bd.fillet(..., radius=...)
Selektieren Sie die kleinste und die größte Fläche der Platte. Geben Sie jeweils Typ und Flächeninhalt aus.
Welche Flächen sind das geometrisch?
Hinweise: .sort_by(SortBy.AREA), .first, .last, .area, .geom_type
Bauen Sie einen Zylinder (radius=12, height=15) mittig auf der Oberseite der Platte auf.
Frage: Warum bd.Plane(oberseite) statt bd.Plane.XY.offset(8)? Was wäre der Unterschied in einem komplexeren Modell?
Hinweise: .faces().sort_by(Axis.Z).last, bd.Plane(face), ebene * zapfen, basis + ...
radius=2.Hinweise: .edges().filter_by(GeomType.CIRCLE), .sort_by(Axis.Z), .last, List-Slicing [-2:], bd.fillet()
Konstruieren Sie einen Flansch (Ringscheibe mit Bohrungen auf einem Lochkreis):
import math
# Ringscheibe: großer Zylinder minus kleiner Zylinder
flansch = bd.Cylinder(radius=40, height=12) - bd.Cylinder(radius=18, height=12)
# Lochkreis: 6 Bohrungen im Abstand von 60°
for winkel in range(0, 360, 60):
x = 30 * math.cos(math.radians(winkel))
y = 30 * math.sin(math.radians(winkel))
flansch = flansch - bd.Pos(x, y) * bd.Cylinder(radius=4, height=12)
flansch
radius=1.Hinweise: .filter_by(GeomType.CYLINDER), .filter_by(GeomType.CIRCLE), .filter_by_position(Axis.Z, ...), bd.fillet(), bd.chamfer()
Laden Sie eine STEP-Datei (z.B. aus FreeCAD, GrabCAD oder dem Kursordner):
teil = bd.import_step("mein_teil.step")
Hinweise: .show_topology(), .faces(), .geom_type, collections.Counter, .filter_by(GeomType.CIRCLE), bd.fillet()